package pl.s7n.paint;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import javax.swing.JPanel;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEditSupport;

import pl.s7n.paint.commands.AbstractStrokeCmd;
import pl.s7n.paint.commands.CmdFactory;
import pl.s7n.paint.commands.FloodFillStrokeCmd;
import pl.s7n.paint.edits.DrawingEditManager;
import pl.s7n.paint.edits.ResizeEditManager;
import pl.s7n.paint.edits.TransformEditManager;
import pl.s7n.paint.utils.Redrawable;
import pl.s7n.paint.utils.UpdatableShape;
import pl.s7n.paint.utils.Utils;

public class DrawableCanvas extends JPanel implements Redrawable {
  private static final long serialVersionUID     = 6521792967019433325L;

  UndoableEditSupport       undoableEditSupport  = new UndoableEditSupport(this);
  DrawingEditManager        drawingEditManager   = new DrawingEditManager(undoableEditSupport, this);
  TransformEditManager      transformEditManager = new TransformEditManager(undoableEditSupport, this);
  ResizeEditManager resizeEditManager = new ResizeEditManager(undoableEditSupport, this);

  AbstractStrokeCmd         action;

  DrawingToolInfo           tool;
  
  ImageWrapper             imageWrapper = new ImageWrapper();

  public static final int   CANVAS_EMPTY_BORDER  = 10;

  public DrawableCanvas(int w, int h) {
    tool = Utils.defaultTool();
    imageWrapper.image = Utils.defaultImage(w, h);

    MouseAdapter ml = new DrawingMouseAdapter();
    addMouseListener(ml);
    addMouseMotionListener(ml);

    setBackground(Color.LIGHT_GRAY);

    Utils.setFixedSize(this, w + CANVAS_EMPTY_BORDER, h + CANVAS_EMPTY_BORDER);
  }

  public void addUndoableEditListener(UndoableEditListener listener) {
    undoableEditSupport.addUndoableEditListener(listener);
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2 = (Graphics2D) g;
    g.translate(CANVAS_EMPTY_BORDER, CANVAS_EMPTY_BORDER);
    g2.drawRenderedImage(imageWrapper.image, transformEditManager.getTransform());

    // draw current action as overlay
    g2.setClip(0, 0, imageWrapper.image.getWidth(), imageWrapper.image.getHeight());
    if (action != null) {
      action.draw(g2);
    }
  }

  private class DrawingMouseAdapter extends MouseAdapter {

    @Override
    public void mousePressed(MouseEvent e) {
      super.mousePressed(e);

      if (contains(e.getPoint())) {
        action = CmdFactory.createCmd(tool.tool);
        action.setTool(tool.clone());
        switch (tool.tool) {
          case LINE:
          case ELLIPSE:
          case RECT:
          case FILL_ELLIPSE:
          case FILL_RECT:
            action.setStart(Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER));
            break;
          case PENCIL:
          case BRUSH:
          case ERASER:
            Point pos = Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER);
            action.setStart(pos);
            Path2D path = new Path2D.Float();
            path.moveTo(pos.x, pos.y);
            action.setShape(path);
            break;
          case FILL:
          case TEXT:
            break;
        }
      }
    }

    @Override
    public void mouseDragged(MouseEvent e) {
      super.mouseDragged(e);

      if (action != null) {
        switch (tool.tool) {
          case LINE:
            action.setShape(Utils.createLine(action.getStart(), Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER)));
            break;
          case PENCIL:
          case BRUSH:
          case ERASER:
            ((Path2D) action.getShape()).lineTo(e.getX() - CANVAS_EMPTY_BORDER, e.getY() - CANVAS_EMPTY_BORDER);
            break;
          case ELLIPSE:
          case FILL_ELLIPSE:
            action.setShape(Utils.createEllipse(action.getStart(), Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER)));
            break;
          case RECT:
          case FILL_RECT:
            action.setShape(Utils.createRectangle(action.getStart(), Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER)));
            break;
        }
        // no need to repaint if dragging FILL
        if (tool.tool != DrawingTools.FILL) {
          repaint();
        }
      }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
      super.mouseReleased(e);
      if (action != null) {
        try {
          switch (tool.tool) {
            case LINE:
            case ELLIPSE:
            case FILL_ELLIPSE:
            case RECT:
            case FILL_RECT:
              break;
            case PENCIL:
            case BRUSH:
            case ERASER:
              ((UpdatableShape) action).updateShape();
              break;
            case FILL:
              FloodFillStrokeCmd ff = (FloodFillStrokeCmd) action;
              ff.setStart(Utils.withBorder(e.getPoint(), -CANVAS_EMPTY_BORDER));
              ff.setCurrentImageRef(imageWrapper.image);
              break;
            default:
              return;
          }

          drawingEditManager.editPerformed(action);

          action.draw(imageWrapper.image.createGraphics());
          redraw(); //TODO delete
          repaint();
        } finally {
          action = null;
        }
      }
    }
  };

  @Override
  public void redraw() {
    Graphics2D g2 = Utils.clearImage(imageWrapper.image);
    drawingEditManager.draw(g2);

    repaint();
  }

  public boolean wasEdited() {
    return drawingEditManager.hasActions() || transformEditManager.hasActions();
  }

  public BufferedImage getImage() {
    return imageWrapper.image;
  }

  public void setImage(BufferedImage image) {
    setImage(image, true);
  }

  public void setImage(BufferedImage image, boolean reset) {
    if (reset) {
      drawingEditManager.reset();
      transformEditManager.reset();
    }
    
    this.imageWrapper.image = image;

    Utils.setFixedSize(this, image.getWidth() + CANVAS_EMPTY_BORDER, image.getHeight() + CANVAS_EMPTY_BORDER);
    repaint();
  }

  public DrawingToolInfo getTool() {
    return tool;
  }

  public void setTool(DrawingToolInfo tool) {
    this.tool = tool;
  }

  public boolean contains(Point p) {
    return p.x >= CANVAS_EMPTY_BORDER //
        && p.y >= CANVAS_EMPTY_BORDER //
        && p.x < imageWrapper.image.getWidth() + CANVAS_EMPTY_BORDER //
        && p.y < imageWrapper.image.getHeight() + CANVAS_EMPTY_BORDER;
  }

  public ImageWrapper getImageWrapper() {
    return imageWrapper;
  }
}
